﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

using NHibernate;
using NHibernate.Criterion;

using SmartProject.BusinessRulePreferences;

using System.Security.Cryptography;
using System.IO;

namespace SmartProject.Entity
{
    public class User : EntityBase
    {
        //private const string InternalPassword = "11561A821F66405fBAB6DB18C12C1602A87F06EA3CCB40979E5C65D31AD7AF73";

        public User()
        {
            SentMessages = new List<Message>();
            ReceivedMessages = new List<Message>();
            Profiles = new List<Profile>();
            Roles = new List<Role>();
            Add(new Username());
        }

        public virtual string Username { get; set; }

        public virtual string FirstName { get; set; }

        public virtual string LastName { get; set; }

        public virtual string Email { get; set; }

        public virtual string Uid { get; private set; }

        public virtual int? FailedLoginCount { get; set; }

        public virtual DateTime? LastLogin { get; set; }

        public virtual DateTime? LastLogout { get; set; }

        public virtual IList<Message> SentMessages { get; set; }

        public virtual IList<Message> ReceivedMessages { get; set; }

        public virtual IList<Profile> Profiles { get; set; }

        public virtual IList<Role> Roles { get; private set; }

        /// <summary>
        /// Gets if the user contains the role name.
        /// </summary>
        /// <param name="roleName">Role name.</param>
        /// <returns>True if the user contains the role name.</returns>
        public virtual bool ContainsRole(string roleName)
        {
            for (int i = 0; i < Roles.Count; i++)
            {
                Role role = Roles[i];
                if (role.Name.ToLower() == roleName.ToLower())
                {
                    return true;
                }
            }
            return false;
        }

        /// <summary>
        /// Gets the encrypted password for an existing user.
        /// </summary>
        public virtual string Password { get; private set; }

        /// <summary>
        /// Gets the first last name.
        /// </summary>
        public virtual string FirstLastName
        {
            get
            {
                return String.Format("{0} {1}", FirstName, LastName);
            }
        }

        /// <summary>
        /// Gets a user by the exact username.
        /// </summary>
        /// <param name="session">Session used in this request.</param>
        /// <param name="userName"></param>
        /// <returns></returns>
        public static User GetUserByUsername(ISession session, string userName)
        {
            IList<ICriterion> expressions = new List<ICriterion>();
            expressions.Add(Restrictions.Eq("Username", userName));
            return ConvertEntities(session, expressions);
        }

        /// <summary>
        /// Gets user by e-mail address.
        /// </summary>
        /// <param name="session">Session used in this request.</param>
        /// <param name="email">E-mail address of the user.</param>
        /// <returns>Null if the user cannot be found.</returns>
        public static User GetUserByEmail(ISession session, string email)
        {
            IList<ICriterion> expressions = new List<ICriterion>();
            expressions.Add(Restrictions.Eq("Email", email).IgnoreCase());
            return ConvertEntities(session, expressions);
        }

        /// <summary>
        /// Gets a list of active users. Note that the Administrator is excluded from the list.
        /// </summary>
        /// <param name="session">Session used in this request.</param>
        /// <returns>Active user list.</returns>
        public static IList<User> GetUsers(ISession session)
        {
            IList<ICriterion> expressions = new List<ICriterion>();
            expressions.Add(Restrictions.Not(Restrictions.Eq("Id", GetUserByUsername(session, "Administrator").Id)));
            return (from p in Find(session, expressions, null, typeof(User)) select p as User).ToList();
        }

        private static User ConvertEntities(ISession session, IList<ICriterion> expressions)
        {
            IList<EntityBase> entities = Find(session, expressions, null, typeof(User));
            if (entities != null && entities.Count == 1)
            {
                User user = (User)entities[0];
                return user;
            }
            return null;
        }

        /// <summary>
        /// Gets user by userName.
        /// </summary>
        /// <param name="session">Session used in this request.</param>
        /// <param name="userName">Username of the user to lookup.</param>
        /// <returns>User instance.</returns>
        public static List<User> GetUsersBySmiliarUsername(ISession session, string userName)
        {
            IList<ICriterion> expressions = new List<ICriterion>();
            expressions.Add(Restrictions.Eq("Username", userName).IgnoreCase());
            IList<EntityBase> entities = Find(session, expressions, null, typeof(User));
            return (from p in entities select p as User).ToList();
        }

        /// <summary>
        /// Updates the user password.
        /// </summary>
        /// <param name="session">Session used in this request.</param>
        /// <param name="oldPassword">Old password.</param>
        /// <param name="newPassword">New password.</param>
        /// <returns>True if the password is changed.</returns>
        public virtual bool ChangePassword(ISession session, string oldPassword, string newPassword)
        {
            if (this.Id == 0)
            {
                throw new InvalidOperationException("You can only change password if this is an existing user.");
            }

            if (this.Password == EncryptString(session, oldPassword, Uid))
            {
                Uid = GetNewUid();
                this.Password = EncryptString(session, newPassword, Uid);
                Save(session);
                return true;
            }
            return false;
        }



        /// <summary>
        /// Saves a user. The user object can be a new instance or an existing one.
        /// </summary>
        /// <param name="session">User instance.</param>
        /// <param name="firstName"></param>
        /// <param name="lastName"></param>
        /// <param name="username"></param>
        /// <param name="email"></param>
        /// <param name="password">Password. Pass in null for an existing user if you do not wish to reset the password.</param>
        /// <param name="roleNames">Role names associated with this user.</param>
        /// <returns></returns>
        public static User NewUser(ISession session, string firstName, string lastName, string username, string email, string password, string[] roleNames)
        {
            if (roleNames == null)
            {
                throw new InvalidOperationException("You must define role(s) for this new user.");
            }

            User user = new User();
            user.FirstName = firstName;
            user.LastName = lastName;
            user.Username = username;
            user.Email = email;

            foreach (string roleName in roleNames)
            {
                user.Roles.Add(Role.GetRole(session, roleName));
            }

            user.Uid = GetNewUid();
            user.Password = EncryptString(session, password, user.Uid);

            user.Save(session);

            if (username != "Administrator")
            {
                Message welcome = new Message();
                welcome.Subject = "Welcome to SmartProject";
                welcome.Description = String.Format("Dear {0} {1}, Welcome to SmartProject.",
                    user.FirstName, user.LastName);
                welcome.Owner = GetUserByUsername(session, "Administrator");
                welcome.Recipient = user;
                welcome.Save(session);
            }
            return user;
        }

        private static string GetNewUid()
        {
            return Guid.NewGuid().ToString().Replace("{", "").Replace("}", "").Replace("-", "").Substring(0, 10);
        }

        /// <summary>
        /// Gets the user instance by user Id.
        /// </summary>
        /// <param name="session">Session used in this request.</param>
        /// <param name="id">User Id.</param>
        /// <returns>User instance.</returns>
        public static User GetUser(ISession session, int id)
        {
            return (User)EntityBase.GetEntity(session, id, typeof(User));
        }

        /// <summary>
        /// Authenticate the username and password.
        /// </summary>
        /// <param name="session">Session used in this request.</param>
        /// <param name="username">Username.</param>
        /// <param name="password">Password.</param>
        /// <returns>True if the username/password are authenticated.</returns>
        public static bool Login(ISession session, string username, string password)
        {
            User user = User.GetUserByUsername(session, username);
            if (user != null)
            {
                if (user.FailedLoginCount != null && user.FailedLoginCount > 3)
                {
                    Exceptions.ExceededFailedLoginCount failed =
                        new Exceptions.ExceededFailedLoginCount(user.Username);
                    Log.Error(session, user, "Login", failed.Message);
                    throw failed;
                }

                if (user.Password == EncryptString(session, password, user.Uid))
                {
                    user.LastLogin = DateTime.Now;
                    using (var transaction = session.BeginTransaction())
                    {
                        user.Save(session);
                        transaction.Commit();
                        transaction.Dispose();
                    }
                    return true;
                }
                else
                {
                    if (user.FailedLoginCount != null)
                    {
                        user.FailedLoginCount += 1;
                    }
                    else
                    {
                        user.FailedLoginCount = 1;
                    }
                    using (var transaction = session.BeginTransaction())
                    {
                        user.Save(session);
                        transaction.Commit();
                        transaction.Dispose();
                    }
                }
            }
            return false;
        }

        /// <summary>
        /// Logs that the user has logged out.
        /// </summary>
        /// <param name="session">Session used in this request.</param>
        /// <param name="userName">Username.</param>
        public static void Logout(ISession session, string userName)
        {
            User user = User.GetUserByUsername(session, userName);
            if (user != null)
            {
                using (var transaction = session.BeginTransaction())
                {
                    user.LastLogout = DateTime.Now;
                    user.Save(session);
                    transaction.Commit();
                    transaction.Dispose();
                }
            }
        }

        /// <summary>
        /// Resets the login for the specific user.
        /// </summary>
        /// <param name="session">Session used in this request.</param>
        /// <param name="userName">Username of the user to reset login for.</param>
        /// <returns>New password.</returns>
        public static string ResetLogin(ISession session, string userName)
        {
            string password = Guid.NewGuid().ToString().Replace("-", "").Substring(0, 10);
            User user = User.GetUserByUsername(session, userName);
            if (user != null)
            {
                using (var transaction = session.BeginTransaction())
                {
                    user.FailedLoginCount = 0;
                    user.Uid = GetNewUid();
                    user.Password = EncryptString(session, password, user.Uid);
                    user.Save(session);
                    transaction.Commit();
                    transaction.Dispose();
                }
            }
            return password;
        }

        /// <summary>
        /// Sends out a message to a recipient.
        /// </summary>
        /// <param name="session">Session used in this request.</param>
        /// <param name="recipient">Username of the recipient.</param>
        /// <param name="subject">Subject line.</param>
        /// <param name="description">Message for the recipient.</param>
        public virtual void Send(ISession session, string recipient, string subject, string description)
        {
            Message message = Message.NewMessage(session, this, subject, description);
            message.Recipient = User.GetUserByUsername(session, recipient);
            message.Save(session);
        }

        /// <summary>
        /// Get new messages send to yourself.
        /// </summary>
        /// <param name="session">Session used in this request.</param>
        /// <returns>A list of new messages.</returns>
        public virtual IList<Message> GetNewMessages(ISession session)
        {
            IList<ICriterion> expressions = new List<ICriterion>();
            expressions.Add(Restrictions.Eq("Recipient", this));
            expressions.Add(Restrictions.IsNull("Opened"));
            IList<Order> orderings = new List<Order>();
            orderings.Add(Order.Asc("Created"));
            IList<EntityBase> entities = Find(session, expressions, orderings, typeof(Message));
            return (from p in entities select p as Message).ToList();
        }

        /// <summary>
        /// Deletes the inactive User instances.
        /// </summary>
        /// <param name="session">Session used in this request.</param>
        public static void Clean(ISession session)
        {
            Delete(session, typeof(User));
        }

        #region Encrypt/Decrypt helper methods.
        private static string _internalCode;
        private static string EncryptString(ISession session, string inputText, string uid)
        {
            if (String.IsNullOrEmpty(_internalCode))
            {
                Preference p = Preference.GetPreference(session, "InternalCode");
                if (p == null)
                {
                    p = new Preference();
                    p.PrefName = "InternalCode";
                    p.PrefValue = String.Format("{0}{1}", System.Environment.OSVersion.Platform, GetNewUid());
                    p.Save(session);
                }
                _internalCode = p.PrefValue;
            }
            string password = _internalCode.Insert(5, String.Format("{0}{1}", _internalCode, uid).ToLower());
            // "Password" string variable is nothing but the key(your secret key) value which is sent from the front end.
            // "InputText" string variable is the actual password sent from the login page.
            // We are now going to create an instance of the
            // Rihndael class.
            RijndaelManaged RijndaelCipher = new RijndaelManaged();
            // First we need to turn the input strings into a byte array.
            byte[] PlainText = System.Text.Encoding.Unicode.GetBytes(inputText);
            // We are using Salt to make it harder to guess our key
            // using a dictionary attack.
            byte[] Salt = Encoding.ASCII.GetBytes(password.Length.ToString());
            // The (Secret Key) will be generated from the specified
            // password and Salt.
            //PasswordDeriveBytes -- It Derives a key from a password
            PasswordDeriveBytes SecretKey = new PasswordDeriveBytes(password, Salt);
            // Create a encryptor from the existing SecretKey bytes.
            // We use 32 bytes for the secret key
            // (the default Rijndael key length is 256 bit = 32 bytes) and
            // then 16 bytes for the IV (initialization vector),
            // (the default Rijndael IV length is 128 bit = 16 bytes)
            byte[] CipherBytes;
            using (ICryptoTransform encryptor = RijndaelCipher.CreateEncryptor(SecretKey.GetBytes(16), SecretKey.GetBytes(16)))
            {
                // Create a MemoryStream that is going to hold the encrypted bytes
                using (MemoryStream memoryStream = new MemoryStream())
                {
                    // Create a CryptoStream through which we are going to be processing our data.
                    // CryptoStreamMode.Write means that we are going to be writing data
                    // to the stream and the output will be written in the MemoryStream
                    // we have provided. (always use write mode for encryption)
                    using (CryptoStream cryptoStream = new CryptoStream(memoryStream, encryptor, CryptoStreamMode.Write))
                    {
                        // Start the encryption process.
                        cryptoStream.Write(PlainText, 0, PlainText.Length);
                        // Finish encrypting.
                        cryptoStream.FlushFinalBlock();
                        // Convert our encrypted data from a memoryStream into a byte array.
                        CipherBytes = memoryStream.ToArray();
                        cryptoStream.Close();
                        cryptoStream.Dispose();
                    }
                    memoryStream.Close();
                    memoryStream.Dispose();
                }
                encryptor.Dispose();
            }
            // Convert encrypted data into a base64-encoded string.
            // A common mistake would be to use an Encoding class for that.
            // It does not work, because not all byte values can be
            // represented by characters. We are going to be using Base64 encoding
            // That is designed exactly for what we are trying to do.
            return Convert.ToBase64String(CipherBytes);
        }
        /*
        private static string DecryptString(string inputText, string uid)
        {
            string decryptedData;
            string password = InternalPassword.Insert(5, uid.ToLower());
            RijndaelManaged RijndaelCipher = new RijndaelManaged();
            byte[] EncryptedData = Convert.FromBase64String(inputText);
            byte[] Salt = Encoding.ASCII.GetBytes(password.Length.ToString());
            PasswordDeriveBytes SecretKey = new PasswordDeriveBytes(password, Salt);
            // Create a decryptor from the existing SecretKey bytes.
            using (ICryptoTransform decryptor = RijndaelCipher.CreateDecryptor(
                SecretKey.GetBytes(16), SecretKey.GetBytes(16)))
            {
                using (MemoryStream memoryStream = new MemoryStream(EncryptedData))
                {
                    // Create a CryptoStream. (always use Read mode for decryption).
                    using (CryptoStream cryptoStream = new CryptoStream(memoryStream, decryptor, CryptoStreamMode.Read))
                    {
                        // Since at this point we don't know what the size of decrypted data
                        // will be, allocate the buffer long enough to hold EncryptedData;
                        // DecryptedData is never longer than EncryptedData.
                        byte[] PlainText = new byte[EncryptedData.Length];
                        // Start decrypting.
                        int DecryptedCount = cryptoStream.Read(PlainText, 0, PlainText.Length);
                        cryptoStream.Close();
                        cryptoStream.Dispose();
                        decryptedData = Encoding.Unicode.GetString(PlainText, 0, DecryptedCount);
                    }
                    memoryStream.Close();
                    memoryStream.Dispose();
                }
                decryptor.Dispose();
            }
            // Return decrypted string.
            return decryptedData;
        }
        */
        #endregion
    }
}
